using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Interop;
using Gildor.SimpleHostMonitor.Desktop.Entities;
using Gildor.SimpleHostMonitor.Desktop.Utilities;

namespace Gildor.SimpleHostMonitor.Desktop
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Main : Window
    {
        #region [Windows API Stuff and System Menu]

        [DllImport ("user32.dll")]
        private static extern IntPtr GetSystemMenu (IntPtr hWnd, bool bRevert);

        [DllImport ("user32.dll")]
        private static extern bool InsertMenu (IntPtr hMenu, Int32 wPosition, Int32 wFlags, Int32 wIDNewItem, string lpNewItem);

        //Define Win32 constants to use.
        const Int32 WM_SYSCOMMAND = 0X112;
        const Int32 MF_SEPARATOR = 0x800;
        const Int32 MF_BYPOSITION = 0x400;
        const Int32 MF_STRING = 0x0;

        //Define menu identities.
        const Int32 _AboutSysMenuID = 0x1000;
        const Int32 _ExitSysMenuID = 0x1001;

        #endregion

        #region [Properties & Fields]

        WindowState _lastWindowState;
        bool _shouldClose;

        /// <summary>
        /// The timer to perform pings.
        /// </summary>
        readonly Timer _pingTimer;

        //int _failureCountToShowBalloonTip = 3;

        delegate void ShowBalloonTipDelegate (params object[] args);

        private readonly HostsConfiguration _config;

        public HostsConfiguration Config
        {
            get { return _config; }
        }

        /// <summary>
        /// Gets the Win32 handle of this window.
        /// </summary>
        /// <value>The handle.</value>
        IntPtr Handle
        {
            get { return (new WindowInteropHelper (this)).Handle; }
        }

        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="Main"/> class.
        /// </summary>
        public Main ()
        {
            #region [Initialize Data]

            _config = ConfigFileLoader.LoadNew ();

            _config.PropertyChanged += (sender, e) =>
            {
                OnConfigurationChanged ();
            };

            //TODO: Move these events into HostsConfiguration.
            _config.Hosts.CollectionChanged +=
                new NotifyCollectionChangedEventHandler (HostList_CollectionChanged);

            _config.Default.PropertyChanged += (sender, e) =>
            {
                if (e.PropertyName == "NotifyStrategy" || e.PropertyName == "FailureThreshold")
                {
                    OnConfigurationChanged ();
                }
            };

            #endregion

            #region [Initialize UI]

            InitializeComponent ();

            if (((App)Application.Current).CommandLineArguments.Contains ("-b"))
            {
                Hide ();
            }

            this.Loaded += Main_Loaded;

            #endregion

            #region [Start Running]

            //Intiatiate the ping timer.
            _pingTimer = new Timer (new TimerCallback (OnTimerTick), null, 2000, 3000);

            this.ConfigurationChanged +=
                delegate { ConfigFileLoader.SaveNew (_config); };

            #endregion
        }

        #region [Data Changed Event Handlers]

        /// <summary>
        /// Handles the CollectionChanged event of the HostList control. 
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.Collections.Specialized.NotifyCollectionChangedEventArgs"/> instance containing the event data.</param>
        /// <remarks>Save the new host list and register the PropertyChanged event of the newly-added host.</remarks>
        private void HostList_CollectionChanged (object sender,
            NotifyCollectionChangedEventArgs e)
        {
            //If a new Host is added, register its PropertyChanged event.
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                foreach (var host in e.NewItems)
                {
                    ((HostInfo)host).PropertyChanged +=
                        new PropertyChangedEventHandler (HostInfo_PropertyChanged);
                }
            }

            //If the host list is changed, save it.
            OnConfigurationChanged ();
        }

        /// <summary>
        /// Handles the PropertyChanged event of the <see cref="HostInfo"/> instance.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.ComponentModel.PropertyChangedEventArgs"/> instance containing the event data.</param>
        private void HostInfo_PropertyChanged (object sender, PropertyChangedEventArgs e)
        {
            switch (e.PropertyName)
            {
                case "Address":
                    //The address is changed, save the new host list.
                    OnConfigurationChanged ();
                    break;
                case "State":
                    //The state of the host is changed, refresh sorter to resort.
                    Dispatcher.Invoke (new Action (() => ListViewSorter.Refresh ()));
                    break;
                case "NotifyStrategy":
                    //The notify strategy is changed, save the new configruation.
                    OnConfigurationChanged ();
                    break;
                default:
                    //Ignore.
                    break;
            }
        }

        #endregion

        #region [System Menu Stuff]
        /// <summary>
        /// Handles the Loaded event of the Main Window.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
        void Main_Loaded (object sender, RoutedEventArgs e)
        {
            //Gets the handle of the system menu.
            IntPtr sysMenuHandle = GetSystemMenu (this.Handle, false);

            //Create new menu items just before the Close menu item.
            InsertMenu (sysMenuHandle, 5, MF_BYPOSITION | MF_SEPARATOR, 0, string.Empty);
            InsertMenu (sysMenuHandle, 6, MF_BYPOSITION, _AboutSysMenuID, "About...");
            InsertMenu (sysMenuHandle, 7, MF_BYPOSITION | MF_SEPARATOR, 0, string.Empty);
            InsertMenu (sysMenuHandle, 8, MF_BYPOSITION, _ExitSysMenuID, "Exit");

            //Attach new WndProc handler.
            HwndSource source = HwndSource.FromHwnd (this.Handle);
            source.AddHook (new HwndSourceHook (WndProc));
        }

        IntPtr WndProc (IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            if (WM_SYSCOMMAND == msg)
            {
                handled = true;
                switch (wParam.ToInt32 ())
                {
                    case _AboutSysMenuID:
                        OnMenuItemAboutClick (this, EventArgs.Empty);
                        break;
                    case _ExitSysMenuID:
                        OnMenuItemExitClick (this, EventArgs.Empty);
                        break;
                    default:
                        handled = false;
                        break;
                }
            }
            return IntPtr.Zero;
        }
        #endregion

        #region  [Monitoring Stuff]

        /// <summary>
        /// Called when [timer tick].
        /// </summary>
        /// <param name="state">The state.</param>
        private void OnTimerTick (object state)
        {
            _config.Hosts.ForEach (host => Pinger.Ping (host.Address,
                new PingCompletedEventHandler (OnPingCompleted),
                new MonitorOperationToken (host, DateTime.Now),
                3000));
        }

        /// <summary>
        /// Called when [ping completed].
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Net.NetworkInformation.PingCompletedEventArgs"/>
        /// instance containing the event data.</param>
        private void OnPingCompleted (object sender, PingCompletedEventArgs e)
        {
            using (sender as Ping)
            {
                var monitorOperationToken = (MonitorOperationToken)e.UserState;

                if (e.Error != null)
                {
                    Dispatcher.Invoke ((LogResultDelegate)logResult,
                        false,
                        monitorOperationToken,
                        int.MaxValue,
                        e.Error.Message,
                        "Error");
                }
                else if (e.Reply.Status != IPStatus.Success)
                {
                    Dispatcher.Invoke (
                        (LogResultDelegate)logResult,
                        false,
                        monitorOperationToken,
                        int.MaxValue,
                        e.Reply.Status.ToString (),
                        e.Reply.Status.ToString ());
                }
                else
                {
                    //If everything is OK, update the display and clear the failure record.
                    Dispatcher.Invoke (
                        (LogResultDelegate)logResult,
                        true,
                        monitorOperationToken,
                        (int)e.Reply.RoundtripTime,
                        e.Reply.RoundtripTime.ToString () + "ms",
                        e.Reply.RoundtripTime.ToString () + "ms");
                }
            }
        }

        private delegate void LogResultDelegate (bool successful,
            MonitorOperationToken monitorOperationToken,
            int logItemValue, string displayMessage, string displayState);

        /// <summary>
        /// Logs the result.
        /// </summary>
        /// <param name="isSuccessful">if set to <c>true</c> [is successful].</param>
        /// <param name="monitorOperationToken">The monitor operation token.</param>
        /// <param name="logItemValue">The log item value.</param>
        /// <param name="displayMessage">The displayed message.</param>
        /// <param name="displayState">The displayed state.</param>
        private void logResult (bool isSuccessful,
            MonitorOperationToken monitorOperationToken,
            int logItemValue, string displayMessage, string displayState)
        {
            var hostInfo = monitorOperationToken.Host;

            //Update the host state.
            hostInfo.State = displayState;

            //Log the result.
            hostInfo.Log.AddLogItem (new MonitorLogItem (
                monitorOperationToken.OperationTime, logItemValue, isSuccessful));

            if (isSuccessful)
            {
                handleSuccess (hostInfo, displayMessage);
            }
            else
            {
                handleFailure (hostInfo, displayMessage);
            }
        }

        /// <summary>
        /// Handles the success.
        /// </summary>
        /// <param name="hostInfo">The host info.</param>
        /// <param name="displayMessage">The message to display.</param>
        private void handleSuccess (HostInfo hostInfo, string displayMessage)
        {
            clearFailure (hostInfo);

            //If the notify strategy of the host is to notify when success, do it.
            if (hostInfo.NotifyStrategy == HostInfo.NotifyStrategies.Success)
            {
                showBalloonTip (hostInfo.Address, "succeeded", displayMessage);
            }
        }

        /// <summary>
        /// Handles the failure.
        /// </summary>
        /// <param name="hostInfo">The host info.</param>
        /// <param name="errorMessage">The error message.</param>
        private void handleFailure (HostInfo hostInfo, string errorMessage)
        {
            //Only show the balloon tip after a site has continuously failed certain times.
            if (hostInfo.NotifyStrategy == HostInfo.NotifyStrategies.Fail &&
                ++hostInfo.FailureCount >= _config.Default.FailureThreshold)
            {
                //First clear the failure record of the host.
                clearFailure (hostInfo);
                //Display the BalloonTip.
                showBalloonTip (hostInfo.Address, "failed", errorMessage);
            }
        }

        /// <summary>
        /// Clears the failure.
        /// </summary>
        /// <param name="hostInfo">The host info.</param>
        private void clearFailure (HostInfo hostInfo)
        {
            hostInfo.FailureCount = 0;
        }

        /// <summary>
        /// Shows the balloon tip.
        /// </summary>
        /// <param name="args">The args.</param>
        private void showBalloonTip (params object[] args)
        {
            if (_config.IsAlertEnabled)
            {
                _trayIcon.ShowBalloon (args);
            }
        }

        #endregion

        #region [Window's Event Handlers]

        /// <summary>
        /// Called when the window state changed.
        /// </summary>
        /// <param name="e">The <see cref="T:System.EventArgs"/> instance containing the event data.</param>
        protected override void OnStateChanged (EventArgs e)
        {
            //Only save the state when it's not minimized.
            if (WindowState.Minimized != WindowState)
            {
                _lastWindowState = WindowState;
            }
        }

        /// <summary>
        /// Called when the window is closing.
        /// </summary>
        /// <param name="e">The <see cref="T:System.ComponentModel.CancelEventArgs"/> containing the event data.</param>
        protected override void OnClosing (CancelEventArgs e)
        {
            if (!_shouldClose)
            {
                e.Cancel = true;
                Hide ();
            }
            else
            {
                //Stop the timer.
                _pingTimer.Change (Timeout.Infinite, Timeout.Infinite);
                _pingTimer.Dispose ();

                //Wait for all pingers to complete.
                Hide ();
                Dispatcher.InvokeShutdown ();
                Thread.Sleep (Pinger.MaxTimeOut);
            }
        }

        /// <summary>
        /// Called when menu item "open" click.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void OnMenuItemOpenClick (object sender, EventArgs e)
        {
            Open ();
        }

        /// <summary>
        /// Called when menu item "Exit" is clicked.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void OnMenuItemExitClick (object sender, EventArgs e)
        {
            _shouldClose = true;
            Close ();
        }

        /// <summary>
        /// Called when menu item "about" clicked.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void OnMenuItemAboutClick (object sender, EventArgs e)
        {
            (new About (this)).ShowDialog ();
        }

        /// <summary>
        /// Called when system tray icon double clicked.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.Input.MouseButtonEventArgs"/> instance containing the event data.</param>
        private void OnSystemTrayIconDoubleClick (object sender, MouseButtonEventArgs e)
        {
            if (e.ChangedButton == MouseButton.Left)
            {
                //Fixed issue: to hide the window when double-clicking the icon in notification area while the window is shown.
                if (this.Visibility == System.Windows.Visibility.Visible)
                {
                    Hide ();
                }
                else
                {
                    Open ();
                }
            }
        }

        /// <summary>
        /// Shows this window and restores the state.
        /// </summary>
        private void Open ()
        {
            Show ();
            WindowState = _lastWindowState;
            //Bring the window to foreground and activate it.
            this.Activate ();
        }

        /// <summary>
        /// Called when system tray balloon tip clicked.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void OnSystemTrayBalloonTipClicked (object sender, EventArgs e)
        {
            //Open ();
        }

        #endregion

        #region [Display Controls' Event Handlers]

        /// <summary>
        /// Called when "Add" button is clicked.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
        private void OnAddHost (object sender, RoutedEventArgs e)
        {
            var address = _siteAddress.Text;
            doAddHost (address);

            //Clear selection after adding a host, or clicking 
            //"Delete" right after clicking "Add" will delete the item just added.
            _hostListView.SelectedIndex = -1;
        }

        private void doAddHost (string address)
        {
            if (_config.Hosts.All (p => string.Compare (p.Address, address, true) != 0))
            {
                _config.Hosts.Add (new HostInfo { Address = address });
            }
        }

        /// <summary>
        /// Called when "Delete" button is clicked.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
        private void OnDeleteHost (object sender, RoutedEventArgs e)
        {
            var address = _siteAddress.Text;
            doDeleteHost (address);
        }

        private void doDeleteHost (string address)
        {
            _config.Hosts.Remove (_config.Hosts.FirstOrDefault (p => p.Address == address));
        }

        /// <summary>
        /// Called when "Update" button is clicked.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
        private void OnUpdateHost (object sender, RoutedEventArgs e)
        {
            _siteAddress.GetBindingExpression (TextBox.TextProperty).UpdateSource ();
        }

        /// <summary>
        /// Called when text box got focus.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
        private void OnTextBoxGotFocus (object sender, RoutedEventArgs e)
        {
            (sender as TextBox).SelectAll ();
        }

        #endregion

        #region [Events]

        #region [ConfigurationChanged Event]

        public event EventHandler<EventArgs> ConfigurationChanged;

        /// <summary>
        /// Raises the <see cref="E:ConfigurationChanged"/> event.
        /// </summary>
        protected virtual void OnConfigurationChanged ()
        {
            if (null != ConfigurationChanged)
            {
                ConfigurationChanged (this, EventArgs.Empty);
            }
        }

        #endregion

        #endregion

    }

    #region [Converters]

    [ValueConversion (typeof (string), typeof (bool))]
    public class StringToBoolNotEmptyConverter : IValueConverter
    {
        public object Convert (object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (typeof (bool) == targetType)
            {
                return (value as string).Length != 0;
            }
            else
            {
                return null;
            }
        }

        public object ConvertBack (object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return null;
        }
    }

    [ValueConversion (typeof (int), typeof (bool))]
    public class IntToBoolNotEqualsConverter : IValueConverter
    {
        public object Convert (object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (typeof (bool) == targetType)
            {
                return (int)value != int.Parse (parameter as string);
            }
            else
            {
                return null;
            }
        }

        public object ConvertBack (object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return null;
        }
    }

    [ValueConversion (typeof (HostInfo.NotifyStrategies), typeof (bool?))]
    public class NotifyStrategyToToggleButtonStateConventer : IValueConverter
    {
        public object Convert (object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (typeof (bool?) != targetType)
            {
                return null;
            }

            switch ((HostInfo.NotifyStrategies)value)
            {
                case HostInfo.NotifyStrategies.Fail:
                    return false;
                //break;
                case HostInfo.NotifyStrategies.Success:
                    return true;
                //break;
                case HostInfo.NotifyStrategies.Never:
                    return null;
                //break;
                default:
                    Debug.Fail ("Unexpected enumeration value.");
                    return false;
                //break;
            }
        }

        public object ConvertBack (object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (typeof (HostInfo.NotifyStrategies) != targetType)
            {
                return null;
            }

            if (value == null)
            {
                return HostInfo.NotifyStrategies.Never;
            }
            else if ((bool)value)
            {
                return HostInfo.NotifyStrategies.Success;
            }
            else
            {
                return HostInfo.NotifyStrategies.Fail;
            }
        }
    }

    [ValueConversion (typeof (HostInfo.NotifyStrategies), typeof (int))]
    public class NotifyStrategyToIndexConventer : IValueConverter
    {
        public object Convert (object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            Debug.Assert (targetType == typeof (int));

            return (int)(HostInfo.NotifyStrategies)value;
        }

        public object ConvertBack (object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            Debug.Assert (targetType == typeof (HostInfo.NotifyStrategies));

            return (HostInfo.NotifyStrategies)(int)value;
        }
    }


    #endregion

}